<!DOCTYPE HTML>
<html>
<head>
  <title>Test for postMessages cloning/transferring objects</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>

<body>
<input id="fileList" type="file"></input>
<script type="application/javascript;version=1.7">

function setup_tests() {
  SpecialPowers.pushPrefEnv({"set": [["dom.input.dirpicker", true],
                                     ["javascript.options.wasm", true]]}, next);
}

function getType(a) {
  if (a === null || a === undefined)
    return 'null';

  if (Array.isArray(a))
    return 'array';

  if (typeof a == 'object')
    return 'object';

  if (SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported() &&
      a instanceof WebAssembly.Module)
    return "wasm";

  return 'primitive';
}

function compare(a, b) {
  is (getType(a), getType(b), 'Type matches');

  var type = getType(a);
  if (type == 'array') {
    is (a.length, b.length, 'Array.length matches');
    for (var i = 0; i < a.length; ++i) {
      compare(a[i], b[i]);
    }

    return;
  }

  if (type == 'object') {
    ok (a !== b, 'They should not match');

    var aProps = [];
    for (var p in a) aProps.push(p);

    var bProps = [];
    for (var p in b) bProps.push(p);

    is (aProps.length, bProps.length, 'Props match');
    is (aProps.sort().toSource(), bProps.sort().toSource(), 'Props match - using toSource()');

    for (var p in a) {
      compare(a[p], b[p]);
    }

    return;
  }

  if (type == 'wasm') {
    var wasmA = new WebAssembly.Instance(a);
    ok(wasmA instanceof WebAssembly.Instance, "got an instance");

    var wasmB = new WebAssembly.Instance(b);
    ok(wasmB instanceof WebAssembly.Instance, "got an instance");

    ok(wasmA.exports.foo() === wasmB.exports.foo(), "Same result!");
    ok(wasmB.exports.foo() === 42, "We want 42");
  }

  if (type != 'null') {
    is (a.toSource(), b.toSource(), 'Matching using toSource()');
  }
}

var clonableObjects = [
  { target: 'all', data: 'hello world' },
  { target: 'all', data: 123 },
  { target: 'all', data: null },
  { target: 'all', data: true },
  { target: 'all', data: new Date() },
  { target: 'all', data: [ 1, 'test', true, new Date() ] },
  { target: 'all', data: { a: true, b:  null, c: new Date(), d: [ true, false, {} ] } },
  { target: 'all', data: new Blob([123], { type: 'plain/text' }) },
  { target: 'all', data: new ImageData(2, 2) },
];

function create_fileList() {
  var url = SimpleTest.getTestFileURL("script_postmessages_fileList.js");
  var script = SpecialPowers.loadChromeScript(url);

  function onOpened(message) {
    var fileList = document.getElementById('fileList');
    SpecialPowers.wrap(fileList).mozSetFileArray([message.file]);

    // Just a simple test
    var domFile = fileList.files[0];
    is(domFile.name, "prefs.js", "fileName should be prefs.js");

    clonableObjects.push({ target: 'all', data: fileList.files });
    script.destroy();
    next();
  }

  script.addMessageListener("file.opened", onOpened);
  script.sendAsyncMessage("file.open");
}

function create_directory() {
  if (navigator.userAgent.toLowerCase().indexOf('Android') != -1) {
    next();
    return;
  }

  var url = SimpleTest.getTestFileURL("script_postmessages_fileList.js");
  var script = SpecialPowers.loadChromeScript(url);

  function onOpened(message) {
    var fileList = document.getElementById('fileList');
    SpecialPowers.wrap(fileList).mozSetDirectory(message.dir);

    fileList.getFilesAndDirectories().then(function(list) {
      // Just a simple test
      is(list.length, 1, "This list has 1 element");
      ok(list[0] instanceof Directory, "We have a directory.");

      clonableObjects.push({ target: 'all', data: list[0] });
      script.destroy();
      next();
    });
  }

  script.addMessageListener("dir.opened", onOpened);
  script.sendAsyncMessage("dir.open");
}

function create_wasmModule() {
  info("Checking if we can play with WebAssembly...");

  if (!SpecialPowers.Cu.getJSTestingFunctions().wasmIsSupported()) {
    next();
    return;
  }

  ok(WebAssembly, "WebAssembly object should exist");
  ok(WebAssembly.compile, "WebAssembly.compile function should exist");

  const wasmTextToBinary = SpecialPowers.unwrap(SpecialPowers.Cu.getJSTestingFunctions().wasmTextToBinary);
  const fooModuleCode = wasmTextToBinary(`(module
    (func $foo (result i32) (i32.const 42))
    (export "foo" $foo)
  )`, 'new-format');

  WebAssembly.compile(fooModuleCode).then((m) => {
    ok(m instanceof WebAssembly.Module, "The WasmModule has been compiled.");
    clonableObjects.push({ target: 'sameProcess', data: m });
    next();
  }, () => {
    ok(false, "The compilation of the wasmModule failed.");
  });
}

function runTests(obj) {
  ok(('clonableObjectsEveryWhere' in obj) &&
     ('clonableObjectsSameProcess' in obj) &&
     ('transferableObjects' in obj) &&
     (obj.clonableObjectsEveryWhere || obj.clonableObjectsSameProcess || obj.transferableObjects), "We must run some test!");

  // cloning tests - everyWhere
  new Promise(function(resolve, reject) {
    if (!obj.clonableObjectsEveryWhere) {
      resolve();
      return;
    }

    var clonableObjectsId = 0;
    function runClonableTest() {
      if (clonableObjectsId >= clonableObjects.length) {
        resolve();
        return;
      }

      var object = clonableObjects[clonableObjectsId++];

      if (object.target != 'all') {
        runClonableTest();
        return;
      }

      obj.send(object.data, []).then(function(received) {
        compare(received.data, object.data);
        runClonableTest();
      });
    }

    runClonableTest();
  })

  // clonable same process
  .then(function() {
    return new Promise(function(resolve, reject) {
      if (!obj.clonableObjectsSameProcess) {
        resolve();
        return;
      }

      var clonableObjectsId = 0;
      function runClonableTest() {
        if (clonableObjectsId >= clonableObjects.length) {
          resolve();
          return;
        }

        var object = clonableObjects[clonableObjectsId++];

        if (object.target != 'sameProcess') {
          runClonableTest();
          return;
        }

        obj.send(object.data, []).then(function(received) {
          compare(received.data, object.data);
          runClonableTest();
        });
      }

      runClonableTest();
    });
  })

  // transfering tests
  .then(function() {
    if (!obj.transferableObjects) {
      return;
    }

    // MessagePort
    return new Promise(function(r, rr) {
      var mc = new MessageChannel();
      obj.send(42, [mc.port1]).then(function(received) {
        ok(received.ports.length, 1, "MessagePort has been transferred");
        mc.port2.postMessage("hello world");
        received.ports[0].onmessage = function(e) {
          ok(e.data, "hello world", "Ports are connected!");
          r();
        }
      });
    });
  })

  // no dup transfering
  .then(function() {
    if (!obj.transferableObjects) {
      return;
    }

    // MessagePort
    return new Promise(function(r, rr) {
      var mc = new MessageChannel();
      obj.send(42, [mc.port1, mc.port1]).then(function(received) {
        ok(false, "Duplicate ports should throw!");
      }, function() {
        ok(true, "Duplicate ports should throw!");
      })
      .then(r);
    });
  })

  // non transfering tests
  .then(function() {
    if (obj.transferableObjects) {
      return;
    }

    // MessagePort
    return new Promise(function(r, rr) {
      var mc = new MessageChannel();
      obj.send(42, [mc.port1]).then(function(received) {
        ok(false, "This object should not support port transferring");
      }, function() {
        ok(true, "This object should not support port transferring");
      })
      .then(r);
    });
  })

  // done.
  .then(function() {
    obj.finished();
  });
}

// PostMessage to the same window.
function test_windowToWindow() {
  info("Testing window to window");
  var resolve;

  onmessage = function(e) {
    if (!resolve) {
      ok(false, "Unexpected message!");
      return;
    }

    let tmp = resolve;
    resolve = null;
    tmp({ data: e.data, ports: e.ports });
  }

  runTests({
    clonableObjectsEveryWhere: true,
    clonableObjectsSameProcess: true,
    transferableObjects: true,
    send: function(what, ports) {
      return new Promise(function(r, rr) {
        resolve = r;

        try {
          postMessage(what, '*', ports);
        } catch(e) {
          resolve = null;
          rr();
        }
      });
    },

    finished: function() {
      onmessage = null;
      next();
    }
  });
}

// PostMessage to iframe
function test_windowToIframe() {
  info("Testing window to iframe");
  test_windowToIframeURL('iframe_postMessages.html');
}

// PostMessage to cross-origin iframe
function test_windowToCrossOriginIframe() {
  info("Testing window to cross-origin iframe");
  test_windowToIframeURL('http://example.com/tests/dom/base/test/iframe_postMessages.html');
}

// iframe helper class
function test_windowToIframeURL(url) {
  var resolve;

  onmessage = function(e) {
    if (!resolve) {
      ok(false, "Unexpected message!");
      return;
    }

    let tmp = resolve;
    resolve = null;
    tmp({ data: e.data, ports: e.ports });
  }

  var ifr = document.createElement('iframe');
  ifr.src = url;
  ifr.onload = function() {
    runTests({
      clonableObjectsEveryWhere: true,
      clonableObjectsSameProcess: true,
      transferableObjects: true,
      send: function(what, ports) {
        return new Promise(function(r, rr) {
          resolve = r;
          try {
            ifr.contentWindow.postMessage(what, '*', ports);
          } catch(e) {
            resolve = null;
            rr();
          }
        });
      },

      finished: function() {
        document.body.removeChild(ifr);
        onmessage = null;
        next();
      }
    });
  }
  document.body.appendChild(ifr);
}

// PostMessage for Workers
function test_workers() {
  info("Testing Workers");

  var resolve;

  var w = new Worker('worker_postMessages.js');
  w.postMessage('workers');
  w.onmessage = function(e) {
    is(e.data, 'ok', "Worker ready!");

    w.onmessage = function(e) {
      if (!resolve) {
        ok(false, "Unexpected message!");
        return;
      }

      let tmp = resolve;
      resolve = null;
      tmp({ data: e.data, ports: e.ports });
    }

    runTests({
      clonableObjectsEveryWhere: true,
      clonableObjectsSameProcess: true,
      transferableObjects: true,
      send: function(what, ports) {
        return new Promise(function(r, rr) {
          resolve = r;
          try {
            w.postMessage(what, ports);
          } catch(e) {
            resolve = null;
            rr();
          }
        });
      },

      finished: function() {
        onmessage = null;
        next();
      }
    });
  }
}

// PostMessage for BroadcastChannel
function test_broadcastChannel() {
  info("Testing broadcastChannel");

  var bc1 = new BroadcastChannel('postMessagesTest');
  var bc2 = new BroadcastChannel('postMessagesTest');

  var resolve;

  bc2.onmessage = function(e) {
    if (!resolve) {
      ok(false, "Unexpected message!");
      return;
    }

    let tmp = resolve;
    resolve = null;
    tmp({ data: e.data, ports: [] });
  }

  runTests({
    clonableObjectsEveryWhere: true,
    clonableObjectsSameProcess: false,
    transferableObjects: false,
    send: function(what, ports) {
      return new Promise(function(r, rr) {
        if (ports.length) {
          rr();
          return;
        }

        resolve = r;
        bc1.postMessage(what);
      });
    },

    finished: function() {
      onmessage = null;
      next();
    }
  });
}

// PostMessage for BroadcastChannel in workers
function test_broadcastChannel_inWorkers() {
  info("Testing broadcastChannel in Workers");

  var bc = new BroadcastChannel('postMessagesTest_inWorkers');
  var resolve;

  var w = new Worker('worker_postMessages.js');
  w.postMessage('broadcastChannel');
  w.onmessage = function(e) {
    is(e.data, 'ok', "Worker ready!");

    w.onmessage = function(e) {
      if (!resolve) {
        ok(false, "Unexpected message!");
        return;
      }

      let tmp = resolve;
      resolve = null;
      tmp({ data: e.data, ports: e.ports });
    }

    runTests({
      clonableObjectsEveryWhere: true,
      clonableObjectsSameProcess: false,
      transferableObjects: false,
      send: function(what, ports) {
        return new Promise(function(r, rr) {
          if (ports.length) {
            rr();
            return;
          }

          resolve = r;
          bc.postMessage(what);
        });
      },

      finished: function() {
        onmessage = null;
        next();
      }
    });
  }
}

// PostMessage for MessagePort
function test_messagePort() {
  info("Testing messagePort");

  var mc = new MessageChannel();
  var resolve;

  mc.port2.onmessage = function(e) {
    if (!resolve) {
      ok(false, "Unexpected message!");
      return;
    }

    let tmp = resolve;
    resolve = null;
    tmp({ data: e.data, ports: e.ports });
  }

  runTests({
    clonableObjectsEveryWhere: true,
    clonableObjectsSameProcess: false,
    transferableObjects: true,
    send: function(what, ports) {
      return new Promise(function(r, rr) {
        resolve = r;
        try {
          mc.port1.postMessage(what, ports);
        } catch(e) {
          resolve = null;
          rr();
        }
      });
    },

    finished: function() {
      onmessage = null;
      next();
    }
  });
}

// PostMessage for MessagePort in Workers
function test_messagePort_inWorkers() {
  info("Testing messagePort in workers");

  var mc = new MessageChannel();
  var resolve;

  var w = new Worker('worker_postMessages.js');
  w.postMessage('messagePort', [ mc.port2 ]);
  w.onmessage = function(e) {
    is(e.data, 'ok', "Worker ready!");

    w.onmessage = function(e) {
      if (!resolve) {
        ok(false, "Unexpected message!");
        return;
      }

      let tmp = resolve;
      resolve = null;
      tmp({ data: e.data, ports: e.ports });
    }

    runTests({
      clonableObjectsEveryWhere: true,
      clonableObjectsSameProcess: false,
      transferableObjects: true,
      send: function(what, ports) {
        return new Promise(function(r, rr) {
          resolve = r;
          try {
            mc.port1.postMessage(what, ports);
          } catch(e) {
            resolve = null;
            rr();
          }
        });
      },

      finished: function() {
        onmessage = null;
        next();
      }
    });
  }
}

var tests = [
  setup_tests,

  create_fileList,
  create_directory,
  create_wasmModule,

  test_windowToWindow,
  test_windowToIframe,
  test_windowToCrossOriginIframe,

  test_workers,

  test_broadcastChannel,
  test_broadcastChannel_inWorkers,

  test_messagePort,
  test_messagePort_inWorkers,
];

function next() {
  if (!tests.length) {
    SimpleTest.finish();
    return;
  }

  var test = tests.shift();
  test();
}

SimpleTest.waitForExplicitFinish();
next();
</script>
</body>
</html>
